import { State, StateContext, StateObject } from "./defination"
import { StateRecord } from "./entity"
import { readonly } from "./readonly"
import { OUTPUT,INPUT, VARIABLE, ERROR, PARENT, CONTEXT, STATE_RECORD, UNDEFINED } from "./shared-symbols"


const _parents = Symbol('parents')


export class CStateObject<
TActivity extends CStateObject<TActivity,TState,TRecord,TContext>,
TState extends State=State,
TRecord extends StateRecord=StateRecord,
TContext extends StateContext=StateContext
> implements StateObject<TActivity,TState,TRecord,TContext> {
  [STATE_RECORD]:TRecord
  [INPUT]?:unknown
  [OUTPUT]?:unknown
  [VARIABLE]?:Record<string|symbol,unknown>
  [ERROR]?:unknown
  [PARENT]?:TActivity
  [CONTEXT]:TContext
  [_parents]?:Array<TActivity>
  constructor(record:TRecord,parent?:TActivity, context?:TContext){
    this[STATE_RECORD] = record
    this[PARENT] = parent
    this[CONTEXT] =  context ??parent?.context ?? {
      serializer:{
        serialize: (value: unknown) => JSON.stringify(value),
        deserialize: (value: unknown) => JSON.parse(value as string)
      }
    } as TContext
    
    
    
  }

  get parent(): TActivity | undefined {
    return this[PARENT]
  }

 
  public get context():TContext {
    return this[CONTEXT]
  }

  get input(): Readonly<unknown>|undefined {
    let input = this[INPUT]
    if (input === undefined) {
      const inputText = this[STATE_RECORD].input
      if (inputText !== undefined) {
        input = this[INPUT] = this[CONTEXT].serializer.deserialize(inputText)
      } else input = this[INPUT] = undefined
    }
    return (input?readonly(input):input) as Readonly<unknown>|undefined
  }
  get variable(): Readonly<Record<string | symbol, unknown>> {
    let variable = this[VARIABLE]
    if (variable === undefined) {
      const variableText = this[STATE_RECORD].variable
      if (variableText !== undefined) {
        variable = this[VARIABLE] = this[CONTEXT].serializer.deserialize(variableText)
      }
    }
    return (variable?readonly(variable):variable) as Readonly<Record<string | symbol, unknown>>
  }
  get output(): Readonly<unknown>  | undefined {
    let result = this[OUTPUT]
    if (result === undefined) {
      const resultText = this[STATE_RECORD].output
      if (resultText !== undefined) {
        result = this[OUTPUT] = this[CONTEXT].serializer.deserialize(resultText)
      }
    }
    return (result?readonly(result):result) as Readonly<unknown>|undefined
  }

  get error(): unknown {
    let error = this[ERROR]
    if (error === undefined) {
      const errorText = this[STATE_RECORD].error
      if (errorText !== undefined) {
        error = this[ERROR] = this[CONTEXT].serializer.deserialize(errorText)
      }
    }
    return (error?readonly(error):error) as unknown
  }

  
  /**
   * return an object of state
   * - it is readonly , if the field is object/array, the field value is also readonly
   * - if the key is $IN, $RET, $VAR, it will return the corresponding value(readonly)
   * - if the key's not the special keys, it will search from current stateObject's output first, then the input, then the variable. if the high priority data 
   * - the the key's value is not found as the previous step, it will search from the parent's internal data (result, input,variable )   *
   * - transparent view feature: if child state values look like this {a: {b:1,c:2} } and parent state values look like this {a: {b:4,d:3} }, 
   *    then the child state's test should be : state.a.b = 1(covered by child),state.a.c = 2(read in child),state.a.d = 3(transprent from parent),state.a.d =3(read in parent)
   * @readonly
   * @type {ReadonlyState}
   * @memberof StateObject
   */
  get state(): TState {
    return new Proxy(this, {
      get: function (target, property, receiver) {
        // handle special keys
        if (property === '$VAR') return target.variable
        if (property === '$IN') return target.input
        if (property === '$OUT') return target.output
        if (property === '$ERR') return target.error
        if (property === '$SUP') return target.parent?.state


        let value:unknown 
        const probabilyObjects: Array<Record<string | symbol, unknown>> = []
        let values = getPropertyValues(target, property)

        // if there is not parent, we can only get the value from the current stateObject
        if (!target[PARENT]) {
          if (values.length === 1) return readonly(values[0])
          if (values.length === 0) return undefined
          for(const val of values){
            if (val && typeof val === 'object') probabilyObjects.push(val as Record<string | symbol, unknown>)  
            else if (value === undefined) value = val
          }
          if (probabilyObjects.length === 0) return value
          if (probabilyObjects.length === 1) return readonly(probabilyObjects[0])
          return createReadonlyState(probabilyObjects)
        }else {
          // we should store the object values int o probabilyObjects
          // right now we dont now whether the property is in the parent's result, variable, input
          for(const val of values){
            if (val && typeof val === 'object') probabilyObjects.push(val as Record<string | symbol, unknown>)  
            else if (value === undefined) value = val
          }
        }
        // there is parent, so we can get the value from the parent's result, variable, input
        const parents = getParents(target as unknown as CStateObject<TActivity,TState,TRecord,TContext>)

        // get all the possible objects from the parents
        for (const parent of parents) {
          const vals = getPropertyValues(parent, property)
          if (vals.length === 0) continue
          for(const val of vals){
            if (val && typeof val === 'object') probabilyObjects.push(val as Record<string | symbol, unknown>)
            else if (value === undefined) value = val
          }
        }
        // if there is no possible objects, return undefined
        if (probabilyObjects.length === 0) return value
        // if there is only one possible object, return the object
        if (probabilyObjects.length === 1) {
          return readonly(probabilyObjects[0])
        }
        // if there is more than one possible object, return a readonly state object
        return createReadonlyState(probabilyObjects)
      },
      set: function (target, property, value, receiver) {
        throw new Error(`property ${String(property)} is readonly`)
      },
      deleteProperty: function (target, property) {
        throw new Error(`property ${String(property)} is readonly`)
      },
      defineProperty: function (target, property, descriptor) {
        throw new Error(`property ${String(property)} is readonly`)
      },
    }) as unknown as TState
  }
}


/**
 *although we have the transparent rule
 we also have a higher  priority rule: 
 result's value should completely cover  the input/variable
 also the input should completely cover the variable value. 
 this logic is implemented in getPropertyValue function
 *
 * @template TSO
 * @param {TSO} target
 * @param {(string | symbol)} property
 * @return {*}  {unknown}
 */
function getPropertyValues<TSO extends StateObject<any,any>>(target: TSO, property: string | symbol): unknown[] {
  const values = []
  if (target.output && property in (target[OUTPUT] as Record<string | symbol, unknown>)) {
      const value= (target[OUTPUT] as Record<string | symbol, unknown>)[property]
      if (value !== undefined) values.push(value)
  }
  if (target.input && property in (target[INPUT] as Record<string | symbol, unknown>)) {
    const value = (target[INPUT] as Record<string | symbol, unknown>)[property]
    if (value !== undefined) values.push(value)
  }
  if (target.variable && property in (target[VARIABLE] as Record<string | symbol, unknown>)) {
    const value = (target[VARIABLE] as Record<string | symbol, unknown>)[property]
    if (value !== undefined) values.push(value)
  }

  
  return values
}

function createReadonlyState(probabilyObject: Array<Record<string | symbol, unknown>>): State {
  return new Proxy(probabilyObject, {
    get: function (target, property, receiver) {
      const nextResult: Array<Record<string | symbol, unknown>> = []
      for (const obj of probabilyObject) {
        if (!(property in obj)) continue
        const value = obj[property]
        if (value ===undefined) return undefined
        if (typeof value === 'object') {
          // hight priority value cover the low priority value
          if (value === null) return null
          nextResult.push(value as Record<string | symbol, unknown>)
        } else {
          return value
        }
      }
      if (nextResult.length > 0) {
        if (nextResult.length === 1) return readonly(nextResult[0])
        return createReadonlyState(nextResult)
      } else {
        return undefined
      }
    },
    set: function (target, property, value, receiver) {
      throw new Error(`property ${String(property)} is readonly`)
    },
  }) as unknown as State
}

export function getParents<
TActivity extends CStateObject<TActivity,TState,TRecord,TContext>,
TState extends State=State,
TRecord extends StateRecord=StateRecord,
TContext extends StateContext=StateContext
>
(stateObject: TActivity): Array<TActivity> {
  let parents = stateObject [_parents]
  if (parents) return parents
  parents = []
  let parent = stateObject[PARENT]
  while (parent) {
    parents.push(parent)
    parent = parent[PARENT] as TActivity
  }
  return stateObject[_parents] = parents
}
